docs(plugin-dev): document the malformed-stack boot posture and pin its division - #19602
Conversation
…ts division Dev boot tolerates and reports; `os validate` / build / publish refuse. Written in the two places `DevPlugin` is documented — its docblock and `content/docs/plugins/packages.mdx`. The reading that had to come first: the two branches are NOT one defect handled two ways. `new AppPlugin(stack)` reads `manifest.id` / `manifest.name` and nothing else — `collections` is a lazy getter first touched in `init()` — so a malformed `packages[]` passes the constructor untouched and is refused one branch later, inside the child-`init()` loop. Measured: the two malformations are exact complements, and a lit healthy control is silent on both instruments. The in-file comment reading "a malformed stack throws HERE" overclaims for that reason. Neither branch refuses today; both already degrade. What the posture adds is the written division and the rule that tolerating is never hiding. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
The docblock ships: measured, its text reaches `dist/index.d.ts` and `dist/index.d.mts`, both under the package's `files[]` — with a positive control (pre-existing docblock prose lands there too). Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6f9f9f910f717bd8a5daa024f16f008941befd11 && git checkout 6f9f9f910f717bd8a5daa024f16f008941befd11
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5c5b67fc4140f76ca3158acea9e0845d9eebfad8 e895dda8b9747b052a54ea2a7fa1155599e45a43 && git checkout -B drift-repro 5c5b67fc4140f76ca3158acea9e0845d9eebfad8 && git merge --no-ff e895dda8b9747b052a54ea2a7fa1155599e45a43
node scripts/docs-audit/affected-docs.mjs --json 5c5b67fc4140f76ca3158acea9e0845d9eebfad8
|
…al actually takes
The superseded sentence ("`collections` is a lazy getter first touched in
`init()`") was false about the mechanism while right about the conclusion.
Measured on this tree, with a falsifier and a lit control:
- `AppPlugin.init()` spans app-plugin.ts 319-395 and its LAST statement is
`ctx.getService('manifest').register(servicePayload)`, built from
`this.bundle` -- `packages[]` intact.
- The `manifest` service is registered by `ObjectQLPlugin.init`
(objectql/src/plugin.ts:430) and its `register()` calls
`resolveArtifactPackageOrder` unguarded as its first statement (:448),
which is what raises INVALID_ARTIFACT_PACKAGE_ENTRY / 422.
- FALSIFIER: the same `init()` on the same malformed bundle, with
`register()` replaced by a no-op, resolves clean. So nothing else
`init()` runs touches `packages[]` -- the `collections` getter included.
- Every `this.collections` read in app-plugin.ts is at line 668 or later,
i.e. inside `start()`; its first read raises the SAME refusal there.
- LIT CONTROL: a healthy stack is silent on both instruments.
Rewritten in every carrier that shipped it: the `DevPlugin` docblock, the
changeset body, the new test file's header, and the sibling
dev-i18n-packages-reader.test.ts comment that named the constructor too.
The two in-file comments the PR body claimed to correct are now actually
corrected (D2 option (a)) -- section 3's catch and section 3b's "inversion"
paragraph -- so the file no longer asserts the old mechanism 150 lines from
the new one.
Docs: `os validate` does not uniformly refuse. A malformed `packages[]`
fails `ObjectStackDefinitionSchema` (measured, with the wrapped-entry
control passing), but a stack with no `manifest` block parses green and
yields the advisory "Missing manifest.id - required for deployment", which
only fails under `--strict` (validate.ts structural-warnings block; exit is
`flags.strict && warnings.length > 0 ? 1 : 0`). The page now says so.
Tests: two new cases pin the init-time path -- the manifest-registration
refusal with its falsifier and lit control, and a whole DevPlugin boot that
tolerates the malformation and reports it on the child-`init()` loop's
error line, with a healthy boot as the silent control.
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
Cosmetic only -- the previous commit left a short line mid-paragraph. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
The at-tier review's fail basis: this PR tightened the `os validate` clause in
two carriers and left it flat in three, while its own changeset called the flat
wording an overstatement.
Three named sites, all comment-only:
- dev-plugin.ts:386 -- the docblock sentence, which ships to dist/*.d.ts and
was a flat universal claim over BOTH rows of its own table.
- dev-plugin-malformed-stack-posture.test.ts:4 -- the file header.
- same file, the comment on the MISSING_IDENTITY case, which asserted the
false half about the very fixture it is attached to.
Extended to two more carriers so the PR does not ship a THIRD posture in 3 of 7
places -- content/docs/plugins/packages.mdx and the changeset -- because the
reading below falsifies their `build` half too, not only `os validate`.
Measured, first-hand, beyond what the review covered:
- `os build` is `compile.ts` (build.ts is `class Build extends Compile`), and
at compile.ts:347 it runs the SAME `ObjectStackDefinitionSchema.safeParse`
validate runs, exiting 1 at :352/:368. So both doors refuse a malformed
`packages[]`.
- compile.ts contains NO `manifest.id` requirement, and its own comment at
:967-971 says the structural advisories are absent because "os compile
never computes them at all (this file has no 'No objects defined' /
'may not do much' string, in any face)".
=> `os build` is SILENT on a stack with no `manifest.id`, so "build refuses
it" was overstated exactly as `os validate` was.
- validate.ts text face re-read at :760-774: `this.exit(1)` fires only inside
`if (flags.strict)`, confirming the `--json` ternary at :740 is not the
only exit and both read one `warnings` list.
- lower-callables.ts:315-319 passes a non-`{ manifest: object }` entry
through untouched, so the schema probe's verdict transfers to what the CLI
actually parses.
- `publish` is not a door this card measured; the claim is dropped rather
than restated.
Zero non-comment changed lines in both source files (classifier lit on a
planted code line). No behaviour change.
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
Ready, green, reviewed — and the last step is blocked with no channelAll three landing preconditions are met and recorded on the card:
This PR has been flipped draft → ready, confirmed by ⛔ ⛔ The seat will not merge this PR, enqueue it by hand, or submit an approving review on it. The only remaining act is enabling auto-merge with Action needed from the maintainer or a seat with the channel: enable auto-merge (SQUASH). Everything else here is finished. Generated by Claude Code |
✅ Correction — this PR is NOT blocked any more. It is in the merge queue.
The maintainer re-granted the permission and the call was retried. ⛔ The earlier standing-down note on this PR — "
|
Fixes #15292
Clause-②: no
Ruling C (
5644710907, director seat, decision batch #123 item 4, maintainer 「同意」 2026-09-12) settled this card and returned it topm:queue. This PR is item 2 (the ordered reading, which came first) and item 3 (the posture text).⛔ It is deliberately not item 1. Making
DevPlugin.initemit the malformed-metadata diagnostic, and adding the skipped app to the CLI startup summary, is the cli seat's sibling PR — the ruling's own State line: 「posture text is the spec seat's; the diagnostic implementation is the cli seat's sibling PR」.Item 2 — the reading, and which arm it lands on
Ruling C, verbatim:
Verdict: arm 1 — two different malformations, and the division is now documented.
With one correction the ruling could not have made from the card: there is no refusal half. Neither branch refuses today. Both already degrade.
Finding the branches
Coordinates were resolved by shape, then checked against the numbers rather than trusted:
dev-plugin.tslast changed on 2026-09-10 (50bc9c73b5, a 9-insert / 9-delete in-place edit), before the ruling — so:505(new AppPlugin(this.options.stack)) and:512(reportOptionalLoadFailure() still land exactly where the card cites them.:856+does not. The card's prose attaches it to "Every childinit()failure is likewise caught ... with exactly one deliberate exception (plugin-dev 没有 ADR-0093 D5 的 fail-fast:dev 栈请求了组织墙但企业包缺失时,只 warn 就继续跑无墙 #5301, organizations)" — that is the child-init()loop, which sits at:891in the post-reader program 6/4 —@objectstack/plugin-dev: the i18n auto-detect never fires for a multi-package app #15232 tree the:505/:512numbers come from. At literal:856in that same tree sits the unrelated@objectstack/restoptional-load catch. The card mixed coordinates from two trees: in the pre-reader program 6/4 —@objectstack/plugin-dev: the i18n auto-detect never fires for a multi-package app #15232 tree (main on the card's filing date, 2026-09-04):856isawait plugin.init(ctx), but there:505is notnew AppPlugin. Resolved in favour of the prose, which is unambiguous.Triggering conditions — measured, with a lit control
new AppPlugin(bundle)— branch:505AppPlugin.init(), i.e. the child-init()loopmanifest.id/manifest.name[AppPlugin] bundle has app payload but no manifest.id / manifest.name— a bareError, no ADR-0112code/statuspackages[]entry with its body inlined instead of wrappedINVALID_ARTIFACT_PACKAGE_ENTRY/422The two defect rows are exact complements, and the control row is silent on both — so neither instrument is stuck-on-throw and neither branch is a second opinion on the other. The control earned its place twice: two earlier probe designs produced a green that the control exposed as meaningless (mocking
@objectstack/objectqlbreaks@objectstack/runtime's own import, collapsing every case onto "runtime not installed"; and a mock context too thin forAppPlugin.initkilled it before the parse, so the malformed case and the healthy case emitted the same line).The load-bearing correction
Before this PR, the in-file comment then at⚠️ past tense on purpose: at head that catch block is
:509read —dev-plugin.ts:573and it says the opposite, because this PR rewrote it:It overclaimed.
AppPlugin’s constructor readsmanifest.id/manifest.nameand nothing else, so the malformation the card actually measured —INVALID_ARTIFACT_PACKAGE_ENTRY— never reaches section 3 at all. It is refused one branch later, fromAppPlugin.init()’s LAST statement:ctx.getService(manifest).register(servicePayload)(app-plugin.ts:394) hands the bundle,packages[]intact, to themanifestservice thatObjectQLPlugin.initregisters (objectql/src/plugin.ts:430), and that service’sregister()callsresolveArtifactPackageOrderunguarded as its first statement (:448). The lazycollectionsgetter is NOT on that path:initspansapp-plugin.ts:319-395and everythis.collectionsread in the file is at:668or later, i.e. instart(). Falsifier: the sameinit()on the same malformed bundle, withregister()replaced by a no-op, resolves clean. Lit control: a healthy stack through the realregister()does not throw. Both comment blocks are corrected in this PR, and the file diff is no longer zero-deletion.Consequences for the ruling's framing
:505, and the structural one is the one that lands in the child-init()loop. Both degrade.@objectstack/plugin-dev: the i18n auto-detect never fires for a multi-package app #15232) is already the reference implementation of the ruled diagnostic: it reaches the same ADR-0130 D4 refusal and names the metadata defect and its remedy, never a package. Recommended as the model for the cli seat's item 1.Item 3 — the posture text
Located page, declared before editing:
content/docs/plugins/packages.mdx(the### @objectstack/plugin-deventry — the only place incontent/docsthat documents the plugin itself rather than mentioning it in passing). Checked against the pages open PRs currently hold (content/docs/automation/flows.mdx,content/docs/references/api/automation-api.mdx,content/docs/references/api/package-api.mdx,content/docs/references/api/protocol.mdx,content/docs/references/automation/flow-function.mdx,content/docs/references/data/object.mdx) — no collision. Noteplugins/packages.mdxis a different file from the heldreferences/api/package-api.mdx.The posture, stated as the
.mdxand the changeset now state it and ⛔ not flat: dev boot tolerates and reports; the doors that refuse are NOT uniform, and they differ by MALFORMATION.os validateos buildos package publishpackages[]ObjectStackDefinitionSchema)compile.tsruns it toomanifestblock + app payload--strictmanifest.id/manifest.nameappear 0 times incompile.ts(LIT CONTROL: plainmanifest= 7 hits, so the zero is a reading)deriveManifestId(package/publish.ts:153, used at:454) falls throughmanifest.id→local.+ slug ofmanifest.name→local.+ slug of the artifact filename (source: 'artifact-filename',:170-172);run()then appliesexplainManifestIdto the derived string (:456), so it refuses only a derived id that is itself invalid. With nomanifestblock the command mints a permanent, immutable identifier out of a filename and proceeds⭐ The⚠️ Arithmetic corrected from an earlier draft: fixing only the three named carriers would not have produced a third posture — it would have left all five door carriers uniformly flat on
buildcolumn is the round-3 correction and it was not charged by the at-tier review — the dev re-read that door on its own and found the.mdxbold sentence and the changeset headline flat in their ownbuildhalf.build, which is this PR's own fail basis recreated on the other half of the same sentence. The substance stands; the count did not. ⛔ Thepublishclaim is dropped from the tree carriers rather than restated: this card never measured that door as part of its deliverable, and an unmeasured claim is the thing this round exists to stop shipping. The cell above is in this body only, it is the at-tier reviewer's reading rather than the card's, and it says the door substitutes — ⛔ so nothing here should be read as a fourth door that refuses.os builddoes not compute the four structural advisories, deliberately, withbuild-json-advisory-parity.e2e.test.tspinning the gap so a fifth dropped list cannot hide in it. What that ruling does NOT reach is narrower, and it is now recorded on its own card: the boot-time identity refusal is a bareErrorwith no ADR-0112code/status. Named here because this PR touches that file.os build --jsoncomputes the four structural advisories — and its ruling says nothing about the boot-time refusal's envelope shape. Citing it as the reason to leave this unfiled was a non-sequitur, and this body's own preceding sentence concedes the gap is narrower than what #11896 covers. The envelope gap is filed as #19617, ⛔ without citing #11896.Plus the division above, and the half that is easy to lose — tolerating is never hiding: a boot that skipped something must never be byte-identical to a healthy one, because a silent degrade is exactly what lets an author or an AI read "it started" as "I wrote it correctly".
Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack→ 90 families, all run at heade895dda8b9, exit codes captured before any pipe: 90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN.exit 3 = PREREQUISITE NOT MET, ⛔ not green —check:skill-examplesandcheck:dual-build-cjs-loads, the latter naming 33 packages with nodist/(「Run pnpm build first. This is NOT a pass: nothing was measured」). A repo-widepnpm build(73/73 tasks) then made both measurable, and exactly those two were re-run: skill-examples exit 0 (258 prose examples type-check across 3 surfaces), dual-build-cjs-loads exit 0 (104/67/620/1 against floors 90/58/520/1). ⛔ Without that build they would stand as NOT MEASURED.pnpm --filter @objectstack/plugin-dev test→ 8 files / 76 tests pass;typecheckclean,check:test-typecheck0 files / 0 errors.pnpm lintrepo-wide (eslint . --no-inline-config): exit 0, run whole, so no narrowing is claimed.pnpm lint(repo-wide,eslint . --no-inline-config): exit 0, run whole, so no narrowing is claimed.pnpm --filter @objectstack/plugin-dev test→ 8 files / 76 tests pass (was 74; +2 cases pinning the measured mechanism).typecheck→ clean, andcheck:test-typecheckconfirms the new test file compiles undertsconfig.test.json(0 errors).7db891a2dc」, which is commit 2 of 4 on this branch (5ae91a51→7db891a2→5f82990a→a3f52cfc→e895dda8) and the head whose review was voided on tier. It contradicted the bullet beside it. Every figure in this block now reads at heade895dda8b9and nowhere else.check-changeset-no-major.mjsprintsLEVEL AXIS: NOT APPLICABLEoutside a PR run — its green here is real on the bump-level axis (it read the changeset and found nomajor) and vacuous on the clause-② axis, which it reads from the PR body. That is whyClause-②: nois on this body.dist/index.d.tsanddist/index.d.mts, both under the package'sfiles[], with a positive control (pre-existing docblock prose lands there too).@objectstack/plugin-dev: patch.Acceptance notes
Observed while reading, not filed and not fixed here — none is a reproducible defect, a contract violation, or a metadata-authoring trap:
packages/plugins/plugin-dev/README.mdis what the docs page links to as the plugin's own reference, and it does not carry the posture. Out of the declared file surface for this claim; worth a follow-up by whoever takes item 1, since that PR is already editing this plugin's operator-facing text.AppPlugin'ssecurityMetadataRegistrarguard is a third way the:505constructor can throw, but it is unreachable fromDevPlugin, which passes one argument. Recorded so the next reader does not count it as a branch.Generated by Claude Code